/*
// rs_respell.cpp : creating dictionaries for revised orthographies (ROs) 
// and using them for conversion between them or traditional orthography (TO)
// in accordance with rfc 4646, the following tag is used:
// en-x-respelled-x-u1
// also used rfc 2616 - http1.1
*/


#include <iostream>
#include <algorithm>
#include "rs_respell.h"
#include "rs_version.h"
using namespace std;
#define version_m (RS_VERSION_BUILD)


rs_respell* rs_command::co_appl = 0;


rs_command::rs_command (
        bool (*a_run)(),
        const rs_string& group_desc,
        const char* a_desc,
        bool hidden) // options and help, separated by \\ 
        :
    co_group (group_desc),
    co_hidden (hidden)
{
    if (co_appl == 0)
        co_appl = new rs_respell ();
    co_run = a_run;
    vector<rs_string> desc_v;
    string_to_vec (rs_stringed(a_desc), desc_v, L"\r");
    if (desc_v.size() > 0) {
        co_name = desc_v[0];
    }
    if (desc_v.size() != 4) {
        rs_report_error (
            L"Incorrect command registration: \\r not occurring three times: ",
            co_name);
        return;
    }
    rs_string arg_str (desc_v[1]);
    unsigned arg_str_len = arg_str.length();
    if (arg_str_len > 0) {
        if (arg_str.substr (arg_str_len-1) == L"\n")
            arg_str = arg_str.substr (0, arg_str_len-1);
        else
            rs_report_error (L"still old way of registering command: ", co_name);
    }
    string_to_vec (arg_str, co_arg_v, L"\n");
    unsigned arg_size = co_arg_v.size();
    co_arg_name_v.resize (arg_size);
    co_arg_type_v.resize (arg_size);
    co_arg_def_v.resize (arg_size);
    co_arg_help_v.resize (arg_size);
    unsigned i;
    for (i=0; i<arg_size; i++)
        parse_argument (co_arg_v[i], co_arg_name_v[i], co_arg_type_v[i],
            co_arg_def_v[i], co_arg_help_v[i]);
    co_help1 = desc_v[2];
    co_help = desc_v[3];
    rs_command::get_appl().add_cmd (this);
}


void rs_command::parse_argument (const rs_string& arg,
                                 rs_string& arg_name,
                                 rs_string& arg_type,
                                 rs_string& arg_def,
                                 rs_string& arg_help)
{
    // "arg" is something like "conv:" or "dict:cm_dict.rst" or "keep/b:-"
    // after slash "/" is arg_type, by default "t" (textual)
    // after colon is default value
    arg_name = nth_substring (arg, 0, L":");
    arg_def = nth_substring (arg, 1, L":");
    arg_help = nth_substring (arg, 2, L":");
    arg_type = nth_substring (arg_name, 1, L"/");
    arg_name = nth_substring (arg_name, 0, L"/");
    if (arg_type == L"")
        arg_type = L"t";
}


rs_respell::rs_respell ()
{
    m_test_cnt=0;
    m_failed_cnt=0;
}
const rs_command* rs_respell::find_cmd (const rs_string& a_cmd_name)
{
    unsigned cmd_size = m_commands.size();
    const rs_string cmd_name(lower_case(a_cmd_name));
    // first, full name
    for (unsigned i=0; i<cmd_size; i++) {
        if (lower_case(m_commands[i]->get_name()) == cmd_name)
            return m_commands[i];
    }
    // second, unique abbreviation, at least 3 characters
    unsigned cmd_name_len = cmd_name.length();
    if (cmd_name_len < 3)
        return 0;
    const rs_command* cmd_found = 0;
    unsigned cmd_cnt=0;  // count of commands found
    for (unsigned i=0; i<cmd_size; i++) {
        if (lower_case(m_commands[i]->get_name().substr (0, cmd_name_len)) == cmd_name) {
            cmd_found = m_commands[i];
            cmd_cnt++;
        }
    }
    if (cmd_cnt == 1)
        return cmd_found;
    rs_report_error (L"Command '" + a_cmd_name + L"' is not " + 
        (cmd_cnt == 0? L"known" : L"unique"));
    return 0;
}

void rs_respell::run_cmd (const rs_string_v& arg_v, int& result, bool& was_processed)
{
    was_processed = false;
    if (arg_v.size() < 2)
        return;
    const rs_command* cmd=find_cmd(arg_v[1]);
    if (cmd == 0)
        return;
    if (parse_args (arg_v, cmd)) {
        result = cmd->run ();
    }
    was_processed = true;
}

int rs_respell::run_all(const rs_string_v& arg_v)
{
    int result;
    result = rs_dispatcher (arg_v);
    if (m_test_cnt>0) {
        rs_log_console (L"Total of " + rs_stringed (m_test_cnt) + L" tests, "
            + rs_stringed(m_failed_cnt) + L" failed");
    }
    return result;
}


void rs_respell::add_arg_value (const rs_string& arg_name,
                                const rs_string& arg_type,
                                const rs_string& arg_value)
{
    if (arg_type == L"t") {
        pair<rs_string, rs_string> name_value (arg_name, arg_value);
        m_arg_t.insert (name_value);
    } else if (arg_type == L"n") {
        unsigned num_value=string_to_unsigned(arg_value); // todo check for incorrect values
        pair<rs_string, unsigned> name_value (arg_name, num_value);
        m_arg_n.insert (name_value);
    } else 
    if (arg_type == L"b") {
        unsigned num_value=0;
        if (arg_value==L"+")
            num_value=1; // 1 meaning true
        else if (arg_value==L"-")
            num_value=0; // 1 meaning false
        pair<rs_string, unsigned> name_value (arg_name, num_value);
        m_arg_n.insert (name_value);
    }
}


bool rs_respell::parse_args (const rs_string_v& arg_v, const rs_command* cmd)
{
    // parses the actual arguments arg_v (from index 2 onwards) for command
    // arguments are either invoked as positional or named
    // named arguments have equals sign (=), positional don't
    // step 1: both textual and numeric arguments are first stored to m_arg_t
    //   first positional, then named
    // step 2: default values are supplied for missing arguments
    // step 3: numeric arguiments are moved from m_arg_t to m_arg_n
    // any argument can only be specified once
    // returns true if parsing went well
    m_arg_n.clear();
    m_arg_t.clear();
    const rs_string_v& desc_name_v = cmd->get_arg_name_v();
    const rs_string_v& desc_type_v = cmd->get_arg_type_v();
    const rs_string_v& desc_def_v = cmd->get_arg_def_v();
    unsigned arg_size = arg_v.size(), i=0;
    // step 1, named and positional arguments; put now all types to m_arg_t
    unsigned pos_arg_ix=0;
    for (i=2; i<arg_size; i++) {
        if (arg_v[i].substr(0,1) == L"/") {
            // named argument
            // e.g. "/dict:cm_dict.rst" (all types) or "/keep" (booleans only)
            rs_string base_arg=arg_v[i].substr(1);
            rs_string act_name = nth_substring (base_arg, 0, L":");
            rs_string act_value = base_arg.substr (act_name.length()); // skip act_name
            if (act_value.substr(0,1) == L":") // skip colon
                act_value = act_value.substr (1);
            rs_string_v::const_iterator it = find (
                desc_name_v.begin(), desc_name_v.end(), act_name);
            if (it == desc_name_v.end()) {
                rs_report_error (L"unknown argument: ", act_name);
                return false;
            }
            unsigned desc_ix = it - desc_name_v.begin();
            if (desc_type_v[desc_ix]==L"b"  &&  act_value.length()==0)
                act_value=L"+"; // booleans have default actual value "+"
            add_arg_value (act_name, desc_type_v[desc_ix], act_value);
        } else {
            // positional argument
            if (pos_arg_ix < desc_name_v.size()) {
                add_arg_value (desc_name_v[pos_arg_ix], desc_type_v[pos_arg_ix], arg_v[i]);
                pos_arg_ix++;
            } else {
                rs_report_error (L"too many arguments");
            }
        }
    }
    // step 2: default values
    unsigned desc_size = desc_name_v.size();
    for (i=0; i<desc_size; i++) {
        const rs_string& desc_name = desc_name_v[i];
        map<rs_string, rs_string>::iterator it = m_arg_t.find (desc_name);
        if (it == m_arg_t.end()) {
            // value not yet set, take default
            add_arg_value (desc_name, desc_type_v[i], desc_def_v[i]);
        }
    }
    return true;
}

void rs_respell::output_help_details (const rs_command *cmd) const
{
    cout << "Command " << stringed (cmd->get_name()) << ":" << endl;
    cout << stringed (cmd->get_help_summary()) << endl << endl;
    cout << "Arguments are:" << endl;
    unsigned arg_size = cmd->get_arg_name_v().size();
    unsigned i;
    for (i=0;  i<arg_size;  i++) {
        const rs_string& arg_type=cmd->get_arg_type_v()[i];
        string type_text;
        if (arg_type==L"t")
            type_text="textual";
        else if (arg_type==L"n")
            type_text="numerical";
        else if (arg_type==L"b")
            type_text="logical";
        rs_string def_value=cmd->get_arg_def_v()[i];
        if (def_value.size() == 0)
            def_value = L"(empty)";
        cout << "  " << (i+1) << "."
            << stringed(cmd->get_arg_name_v()[i]) << ": " << type_text
            << ", default value: "
            << stringed(def_value) << endl;
        const rs_string& arg_help=cmd->get_arg_help_v()[i];
        if (arg_help.size()>0)
            cout << "    " << stringed(arg_help) << endl;
    }
    cout << endl;
    cout << stringed (cmd->get_help_details()) << endl;
}


bool rs_respell::cmd_line_help (const rs_string& arg)
{
    unsigned cmd_size = m_commands.size(), i;
    if (arg.size() == 0)  {
        cout << "Possible commands for program respell.exe are" << endl;
        const rs_string* last_group=&(rs_string(L""));
        for (i=0; i<cmd_size; i++) {
            if (m_commands[i]->is_hidden())
                continue;
            const rs_string* this_group=&m_commands[i]->get_group();
            if (*last_group != *this_group) {
                last_group = this_group;
                cout << "Group: " << stringed(*this_group) << " commands" << endl;
            }
            cout << "  " << stringed (m_commands[i]->get_name()) << ": "
                << stringed (m_commands[i]->get_help_summary()) << endl;
        }
        cout
            << "More help on commands can be displayed with " <<
            "'respell help <cmd>'" << endl
            << "'respell help arguments' describes command-line syntax" << endl
            << "'respell help all' shows all help texts" << endl;
        /*
        cout << "  gcafr in.rst in_col_name opt_max_afrs" << endl
        << "    tests affixes by decomposing words in table" << endl;
        */
        return true;
    }
    const rs_command *cmd = rs_command::get_appl().find_cmd (arg);
    if (cmd == 0) {
        rs_report_error (L"Unknown command to get help on: ", arg);
        return false;
    }
    if (cmd->get_name() == L"arguments") {
        return cmd->run();
    }
    bool all_help = cmd->get_name() == L"all";
    if (all_help) {
        for (i=0; i<cmd_size; i++)
            if (!m_commands[i]->is_hidden()) {
                output_help_details (m_commands[i]);
                cout << endl << endl;
            }
    } else
        output_help_details (cmd);
    return true;
}

int rs_respell::rs_dispatcher(const rs_string_v& arg_v)
{ // returns 1 if ok, 0 if unknown command
    size_t argc = arg_v.size();
    if (argc<2) {
        cout << "respell.exe version " << version_m << ", copyright (c) 2008-2011 Joze Fabcic (Yozheh Fabchich)" << endl
            << "To get help, use command 'respell.exe help'" << endl;
        return 1;
    }
    if (arg_v[1] == L"-h"   ||
        arg_v[1] == L"/h"   ||
        arg_v[1] == L"/?") {
        cmd_line_help(arg_v[1]);
        return 1;
    }
    int result;
    bool was_processed;
    run_cmd (arg_v, result, was_processed);
    return result;
}


bool rs_respell::parse_and_execute_line (const rs_string& cmd_line)
{ // returns false if execution sequence is to be stopped
    rs_string_v arg_v;
    string_to_vec_arg (cmd_line, arg_v);
    if (arg_v.size()==0)
        return true;
    if (arg_v[0][0] == L'#')
        return true;
    if (arg_v[0] == L"return"  ||  arg_v[0] == L"return")
        return false;
    arg_v.insert (arg_v.begin(), L"respell.exe");
    rs_log_console (L"Running: " + cmd_line);
    /*
    for (i=0; i<argc; i++)
        cout << i << ": " << argv[i] << "; ";
    cout << endl;
    */
    if (rs_dispatcher(arg_v) == 0)
        return false;
    return true;
}


bool rs_respell::run_batch (const rs_string& batch_fname)
{
    rs_istream ifs (batch_fname);
    if (!ifs.good())
        return false;
    wstring line;
    while (!ifs.eof()) {
        if (!ifs.read_line (line))
            return false;
        if (!parse_and_execute_line (line))
            return false;
    }
    return true;
}


rs_string& rs_respell::t (const rs_string& arg_name)
{
    map<rs_string, rs_string>::iterator it = m_arg_t.find (arg_name);
    if (it == m_arg_t.end()) {
        rs_report_error (L"Unknown argument name: ", arg_name);
        return m_tt;
    }
    return it->second;
}


unsigned&  rs_respell::n (const rs_string& arg_name)
{
    map<rs_string, unsigned>::iterator it = m_arg_n.find (arg_name);
    if (it == m_arg_n.end()) {
        rs_report_error (L"Unknown argument name: ", arg_name);
        return m_nn;
    }
    return it->second;
}


int main(int argc, const char* argv[])
{
    int result, i;
    _set_sbh_threshold (1016);
    rs_string_v arg_vec;
    for (i=0; i<argc; i++)
        arg_vec.push_back (rs_stringed (argv[i]));
    try {
        log_ofs = new rs_ostream (L"respell.log");
        result = rs_command::get_appl().run_all (arg_vec);
        delete log_ofs;
        log_ofs = 0;
    }  catch (exception& e) {
        cout << "error: exception " << e.what() << endl;
    } catch (...) {
        cout << "error: internal error" << endl;
    }
    return result;
}
